Convex Schema Validator
Define and validate database schemas in Convex with proper typing, index configuration, optional fields, unions, and strategies for schema migrations.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
Primary:
https://docs.convex.dev/database/schemas
Indexes:
https://docs.convex.dev/database/indexes
Data Types:
https://docs.convex.dev/database/types
For broader context:
https://docs.convex.dev/llms.txt
Instructions
Basic Schema Definition
// convex/schema.ts
import
{
defineSchema
,
defineTable
}
from
"convex/server"
;
import
{
v
}
from
"convex/values"
;
export
default
defineSchema
(
{
users
:
defineTable
(
{
name
:
v
.
string
(
)
,
email
:
v
.
string
(
)
,
avatarUrl
:
v
.
optional
(
v
.
string
(
)
)
,
createdAt
:
v
.
number
(
)
,
}
)
,
tasks
:
defineTable
(
{
title
:
v
.
string
(
)
,
description
:
v
.
optional
(
v
.
string
(
)
)
,
completed
:
v
.
boolean
(
)
,
userId
:
v
.
id
(
"users"
)
,
priority
:
v
.
union
(
v
.
literal
(
"low"
)
,
v
.
literal
(
"medium"
)
,
v
.
literal
(
"high"
)
)
,
}
)
,
}
)
;
Validator Types
Validator
TypeScript Type
Example
v.string()
string
"hello"
v.number()
number
42
,
3.14
v.boolean()
boolean
true
,
false
v.null()
null
null
v.int64()
bigint
9007199254740993n
v.bytes()
ArrayBuffer
Binary data
v.id("table")
Id<"table">
Document reference
v.array(v)
T[]
[1, 2, 3]
v.object({})
{ ... }
{ name: "..." }
v.optional(v)
T | undefined
Optional field
v.union(...)
T1 | T2
Multiple types
v.literal(x)
"x"
Exact value
v.any()
any
Any value
v.record(k, v)
Recordhttps://api.dicebear.com/7.x/initials/svg?seed=
${
user
.
name
}
,
}
)
;
}
return
users
.
length
;
}
,
}
)
;
Making Optional Fields Required
// Step 1: Backfill all null values
// Step 2: Update schema to required
users
:
defineTable
(
{
name
:
v
.
string
(
)
,
email
:
v
.
string
(
)
,
avatarUrl
:
v
.
string
(
)
,
// Now required after backfill
}
)
Examples
Complete E-commerce Schema
// convex/schema.ts
import
{
defineSchema
,
defineTable
}
from
"convex/server"
;
import
{
v
}
from
"convex/values"
;
export
default
defineSchema
(
{
users
:
defineTable
(
{
email
:
v
.
string
(
)
,
name
:
v
.
string
(
)
,
role
:
v
.
union
(
v
.
literal
(
"customer"
)
,
v
.
literal
(
"admin"
)
)
,
createdAt
:
v
.
number
(
)
,
}
)
.
index
(
"by_email"
,
[
"email"
]
)
.
index
(
"by_role"
,
[
"role"
]
)
,
products
:
defineTable
(
{
name
:
v
.
string
(
)
,
description
:
v
.
string
(
)
,
price
:
v
.
number
(
)
,
category
:
v
.
string
(
)
,
inventory
:
v
.
number
(
)
,
isActive
:
v
.
boolean
(
)
,
}
)
.
index
(
"by_category"
,
[
"category"
]
)
.
index
(
"by_active_and_category"
,
[
"isActive"
,
"category"
]
)
.
searchIndex
(
"search_products"
,
{
searchField
:
"name"
,
filterFields
:
[
"category"
,
"isActive"
]
,
}
)
,
orders
:
defineTable
(
{
userId
:
v
.
id
(
"users"
)
,
items
:
v
.
array
(
v
.
object
(
{
productId
:
v
.
id
(
"products"
)
,
quantity
:
v
.
number
(
)
,
priceAtPurchase
:
v
.
number
(
)
,
}
)
)
,
total
:
v
.
number
(
)
,
status
:
v
.
union
(
v
.
literal
(
"pending"
)
,
v
.
literal
(
"paid"
)
,
v
.
literal
(
"shipped"
)
,
v
.
literal
(
"delivered"
)
,
v
.
literal
(
"cancelled"
)
)
,
shippingAddress
:
v
.
object
(
{
street
:
v
.
string
(
)
,
city
:
v
.
string
(
)
,
state
:
v
.
string
(
)
,
zip
:
v
.
string
(
)
,
country
:
v
.
string
(
)
,
}
)
,
createdAt
:
v
.
number
(
)
,
updatedAt
:
v
.
number
(
)
,
}
)
.
index
(
"by_user"
,
[
"userId"
]
)
.
index
(
"by_user_and_status"
,
[
"userId"
,
"status"
]
)
.
index
(
"by_status"
,
[
"status"
]
)
,
reviews
:
defineTable
(
{
productId
:
v
.
id
(
"products"
)
,
userId
:
v
.
id
(
"users"
)
,
rating
:
v
.
number
(
)
,
comment
:
v
.
optional
(
v
.
string
(
)
)
,
createdAt
:
v
.
number
(
)
,
}
)
.
index
(
"by_product"
,
[
"productId"
]
)
.
index
(
"by_user"
,
[
"userId"
]
)
,
}
)
;
Using Schema Types in Functions
// convex/products.ts
import
{
query
,
mutation
}
from
"./_generated/server"
;
import
{
v
}
from
"convex/values"
;
import
{
Doc
,
Id
}
from
"./_generated/dataModel"
;
// Use Doc type for full documents
type
Product
=
Doc
<
"products"
; // Use Id type for references type ProductId = Id < "products"
; export const get = query ( { args : { productId : v . id ( "products" ) } , returns : v . union ( v . object ( { _id : v . id ( "products" ) , _creationTime : v . number ( ) , name : v . string ( ) , description : v . string ( ) , price : v . number ( ) , category : v . string ( ) , inventory : v . number ( ) , isActive : v . boolean ( ) , } ) , v . null ( ) ) , handler : async ( ctx , args ) : Promise < Product | null
=> { return await ctx . db . get ( args . productId ) ; } , } ) ; Best Practices Never run npx convex deploy unless explicitly instructed Never run any git commands unless explicitly instructed Always define explicit schemas rather than relying on inference Use descriptive index names that include all indexed fields Start with optional fields when adding new columns Use discriminated unions for polymorphic data Validate data at the schema level, not just in functions Plan index strategy based on query patterns Common Pitfalls Missing indexes for queries - Every withIndex needs a corresponding schema index Wrong index field order - Fields must be queried in order defined Using v.any() excessively - Lose type safety benefits Not making new fields optional - Breaks existing data Forgetting system fields - _id and _creationTime are automatic References Convex Documentation: https://docs.convex.dev/ Convex LLMs.txt: https://docs.convex.dev/llms.txt Schemas: https://docs.convex.dev/database/schemas Indexes: https://docs.convex.dev/database/indexes Data Types: https://docs.convex.dev/database/types